package org.mockfu;

import static org.mockfu.Exceptions.*;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;

/**
 * Normalized MethodVisitor that provides some useful bytecode generation operations.
 */
class NormalMethodGenerator extends GeneratorAdapter
{

    public NormalMethodGenerator(MethodVisitor mv, int access, String name, String desc)
    {
        super(mv, access, name, desc);
    }


    public Method getMethod(String owner, String name, String desc)
    {
        try
        {
            /* find method object and owning class (real owner may in fact be super class) */
            Method method = null;
            Class<?> owningClass = Class.forName(Type.getObjectType(owner).getClassName());
            Class<?>[] params = getClasses(Type.getArgumentTypes(desc));

            for (Class<?> nextClass = owningClass; nextClass != null; nextClass = nextClass.getSuperclass())
            {
                try
                {
                    method = nextClass.getDeclaredMethod(name, params);
                    owningClass = nextClass;
                    break;
                }
                catch (NoSuchMethodException ex)
                {
                }
            }

            if (method == null)
                throw new NoSuchMethodException(name);

            return method;
        }
        catch (Exception ex)
        {
            throw undeclared(ex);
        }
    }

    public Field getField(String owner, String name, String desc)
    {
        try
        {
            /* find field object and owning class (owner may in fact be super class) */
            Field field = null;
            Class<?> owningClass = Class.forName(Type.getObjectType(owner).getClassName());

            for (Class<?> nextClass = owningClass; nextClass != null; nextClass = nextClass.getSuperclass())
            {
                try
                {
                    field = nextClass.getDeclaredField(name);
                    owningClass = nextClass;
                    break;
                }
                catch (NoSuchFieldException ex)
                {
                }
            }

            if (field == null)
                throw new NoSuchFieldException(name);

            return field;
        }
        catch (Exception ex)
        {
            throw undeclared(ex);
        }
    }

    public void invokeVirtual(String owner, String name, String desc)
    {
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, name, desc);
    }

    public void invokeSpecial(String owner, String name, String desc)
    {
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, desc);
    }

    public void invokeInterface(String owner, String name, String desc)
    {
        mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, owner, name, desc);
    }

    public void invokeStatic(String owner, String name, String desc)
    {
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, desc);
    }

    public void pushNull()
    {
        push((String)null);
    }

    public void pushDefault(Type type)
    {
        switch (type.getSort())
        {
        case Type.BOOLEAN:
        case Type.BYTE:
        case Type.SHORT:
        case Type.CHAR:
        case Type.INT:
            push(0);
            break;
        case Type.LONG:
            push(0L);
            break;
        case Type.FLOAT:
            push(0.0f);
            break;
        case Type.DOUBLE:
            push(0.0);
            break;
        case Type.VOID:
            break;
        default:
            pushNull();
            break;
        }
    }

    public void pushBoxType(Type type)
    {
        Type classType = Type.getType(Class.class);

        switch (type.getSort())
        {
        case Type.BOOLEAN:
            getStatic(Type.getType(Boolean.class), "TYPE", classType);
            break;
        case Type.BYTE:
            getStatic(Type.getType(Byte.class), "TYPE", classType);
            break;
        case Type.SHORT:
            getStatic(Type.getType(Short.class), "TYPE", classType);
            break;
        case Type.CHAR:
            getStatic(Type.getType(Character.class), "TYPE", classType);
            break;
        case Type.INT:
            getStatic(Type.getType(Integer.class), "TYPE", classType);
            break;
        case Type.LONG:
            getStatic(Type.getType(Long.class), "TYPE", classType);
            break;
        case Type.FLOAT:
            getStatic(Type.getType(Float.class), "TYPE", classType);
            break;
        case Type.DOUBLE:
            getStatic(Type.getType(Double.class), "TYPE", classType);
            break;
        case Type.VOID:
            break;
        default:
            super.push(type);
            break;
        }
    }

    /**
     * Creates an Object array containing the values from the top of the stack, saving the array to
     * a local variable.
     *
     * @param argumentTypes
     * @return the local variable slot containing the new array
     */
    public int storeArray(Type[] argumentTypes)
    {
        Type objectType = Type.getType(Object.class);
        Type arrayType = Type.getType(Object[].class);

        /* create new local variable slot for array */
        int arraySlot = newLocal(arrayType);
        /* create array and put it into slot */
        push(argumentTypes.length);
        newArray(objectType);
        storeLocal(arraySlot);

        for (int i = argumentTypes.length - 1 ; i >= 0 ; i--)
        {
            box(argumentTypes[i]);
            /* add to array at index i */
            loadLocal(arraySlot);
            swap();
            push(i);
            swap();
            arrayStore(objectType);
        }

        return arraySlot;
    }

    /**
     * Creates a literal class array on the top of the stack.
     *
     * @param array the literal class array
     * @param arrayType the type of the new
     */
    public void pushClassArray(Class<?>[] array)
    {
        Type arrayType = Type.getType(Class.class);

        push(array.length);
        newArray(arrayType);

        for (int i = 0 ; i < array.length ; i++)
        {
            dup();
            push(i);
            pushBoxType(Type.getType(array[i]));
            arrayStore(arrayType);
        }
    }

    public Class<?> getClass(Type type)
    {
        try
        {
            switch (type.getSort())
            {
            case Type.ARRAY:
                int dimensions[] = new int[type.getDimensions()];
                Class<?> elementClass = getClass(type.getElementType());
                Object array = Array.newInstance(elementClass, dimensions);
                return array.getClass();
            case Type.OBJECT:
                return Class.forName(type.getClassName());
            case Type.BOOLEAN:
                return boolean.class;
            case Type.BYTE:
                return byte.class;
            case Type.SHORT:
                return short.class;
            case Type.INT:
                return int.class;
            case Type.CHAR:
                return char.class;
            case Type.LONG:
                return long.class;
            case Type.FLOAT:
                return float.class;
            case Type.DOUBLE:
                return double.class;
            }

            return null;
        }
        catch (Exception ex)
        {
            throw undeclared(ex);
        }
    }

    public Class<?>[] getClasses(Type[] types)
    {
        Class<?>[] classes = new Class[types.length];

        for (int i = 0 ; i < types.length ; i++)
            classes[i] = getClass(types[i]);

        return classes;
    }
}
